package jmine.tec.environment.db;

import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Collections;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Properties;

import javax.sql.DataSource;

import jmine.tec.environment.db.server.ManagedTestDBServer;
import jmine.tec.environment.db.server.TestDBServer;
import jmine.tec.environment.db.server.TestDBServerException;
import jmine.tec.environment.utils.BuildData;
import jmine.tec.environment.utils.SpringConfigFactory;
import jmine.tec.persist.schema.creation.FullSchemaCreator;
import jmine.tec.utils.UtilsMessages;

import org.apache.commons.dbcp.BasicDataSource;
import org.apache.log4j.Logger;
import org.hibernate.Session;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;

import bancosys.tec.persist.audit.AuditManager;
import bancosys.tec.persist.authorization.AuthorizationContext;
import bancosys.tec.persist.authorization.AuthorizationManager;
import bancosys.tec.persist.authorization.AuthorizationStatus;
import bancosys.tec.persist.hibernate.PartitionSessionFactory;
import bancosys.tec.persist.spring.transaction.PartitionTransactionDefinition;
import bancosys.tec.persist.validator.ValidatorFactory;
import bancosys.tec.utils.db.converter.CouldNotConvertException;
import bancosys.tec.utils.db.converter.DbConverter;
import bancosys.tec.utils.db.descriptor.FlatReferenceDatabaseBuilder;
import bancosys.tec.utils.db.descriptor.FlatReferenceDatabaseBuilder.ReferenceDatabaseDescriptionType;
import bancosys.tec.utils.db.descriptor.ReferenceDatabaseDescription;
import bancosys.tec.utils.db.descriptor.ReferenceDatabaseEntry;
import bancosys.tec.utils.db.descriptor.ReferenceDatabaseException;
import bancosys.tec.utils.db.descriptor.ReferenceDatabaseParser;
import bancosys.tec.utils.db.executor.CouldNotExecuteException;
import bancosys.tec.utils.db.executor.DbExecutor;

/**
 * Helper que armazena os recursos de banco de dados utilizado pelos testes. <br>
 */
public class DBLoadedServerUtils {

    public static final String CONFIG_FILE = "conf/components.xml";

    private static final Logger LOG = Logger.getLogger(DBLoadedServerUtils.class);

    protected BeanFactory bf = null;

    protected PartitionSessionFactory sessionFactory = null;

    protected ValidatorFactory validatorFactory = null;

    protected AuditManager auditManager = null;

    protected PartitionTransactionDefinition txDefinition = null;

    private TestDBServer server;

    private PlatformTransactionManager txManager = null;

    private String[] refDbResources;

    private final List<ReferenceDatabaseDescription> refdbDescriptions;

    private List<URL> sqlData;

    private final DBUtilsFactory dbUtilsFactory;

    // private final SpringConfigFactory configFactory;

    private final AuthorizationManager authorizationManager;

    private String environmentKey;

    private String schemaKey;

    private String schemaMD5;

    private String[] schemaStatements;

    private Map<ReferenceDatabaseDescriptionType, String> dataKey;

    private Map<ReferenceDatabaseDescriptionType, String> dataMD5;

    // Conexão reutilizada.
    private Connection connection = null;

    /**
     * Inicializa todos os recursos para execução de testes, incluindo, spring, datasource e TestDBServer.<br/>
     * Caso o schemaCreation seja MANUAL e o paramtro sqlTableSchema seja nulo ou vazio, o schema será gerado a partir das classes mapeadas.
     * Para que isso ocorra, é necessário que TODAS as classes estejam 'bem mapeadas' (passar no BOStaticAnalysisTest)<br/>
     * O parâmetro sqlExtraSchema existe para os casos em que o schemaCreation é manual e sqlTableSchema é vazio ou nulo e existam elementos
     * da base que não podem ser gerados, tais como VIEWs.
     * 
     * @param bf a beanFactory.
     * @param environmentKey a chave do ambiente.
     * @param databaseServerType String
     * @param refDbResources os arquivos com recursos de base de referência.
     */
    public DBLoadedServerUtils(BeanFactory bf, String environmentKey, String databaseServerType, String[] refDbResources) {
        this.bf = bf;
        this.refDbResources = refDbResources;
        this.environmentKey = environmentKey;
        try {
            this.refdbDescriptions = new ReferenceDatabaseParser().getDescriptions(refDbResources);
        } catch (ReferenceDatabaseException e) {
            throw new TestDBServerException(e);
        }

        System.setProperty("org.xml.sax.driver", "org.apache.xerces.parsers.SAXParser");
        System.setProperty("p6.home", System.getProperty("user.home") + File.separator + ".jmine" + File.separator + "tec.test");

        this.initializeTestDBServer();
        this.dbUtilsFactory = new DBUtilsFactory(this.server);

        // Respeitar a configuração de upperCase ao montar a base de dados
        Boolean upperCaseEnabled = (Boolean) this.bf.getBean("upperCase");
        this.dbUtilsFactory.setUpperCaseEnabled(upperCaseEnabled);

        this.sessionFactory = (PartitionSessionFactory) this.bf.getBean("sessionFactory");
        this.validatorFactory = (ValidatorFactory) this.bf.getBean("validatorFactory");
        this.auditManager = (AuditManager) this.bf.getBean("auditManager");
        this.authorizationManager = (AuthorizationManager) this.bf.getBean("boAuthorizationManager");

        this.txManager = (PlatformTransactionManager) this.bf.getBean("transactionManager");
        this.txDefinition = new PartitionTransactionDefinition();
        this.txDefinition.setPartition(AuthorizationContext.hot);
        this.txDefinition.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);

        FullSchemaCreator creator = new FullSchemaCreator(this.server.getDialect());
        this.schemaStatements = creator.createStatements(this.getSessionFactory().getConfiguration());

        this.schemaKey = BuildData.getSchemaKey(this.environmentKey);

        this.schemaMD5 = BuildData.generateSchemaMD5(this.schemaStatements, this.getRefdb(ReferenceDatabaseDescriptionType.SCHEMA));

        this.dataKey = new HashMap<ReferenceDatabaseDescriptionType, String>();
        this.dataMD5 = new HashMap<ReferenceDatabaseDescriptionType, String>();
        for (ReferenceDatabaseDescriptionType type : ReferenceDatabaseDescriptionType.values()) {
            this.dataKey.put(type, BuildData.getDataKey(this.environmentKey, databaseServerType, type));
            this.dataMD5.put(type, BuildData.generateDataMD5(this.getRefdb(type), this.getSchemaMD5()));
        }
    }

    /**
     * Recupera do spring a instância de TestDBServer a ser utilizada.
     * 
     * @return a instância de TestDBServer a ser utilizada.
     */
    public TestDBServer createTestDBServerInstance() {
        return (TestDBServer) this.bf.getBean(SpringConfigFactory.DB_SERVER_BEAN_NAME, TestDBServer.class);
    }

    /**
     * Recupera do spring a instância de datasource sem dados carregados a ser utilizada.
     * 
     * @return a instância de datasource a ser utilizada.
     */
    public DataSource createDataSourceInstance() {
        return (DataSource) this.bf.getBean(SpringConfigFactory.DATA_SOURCE_BEAN_NAME, DataSource.class);
    }

    /**
     * Devolve o schema configurado como padrão no hibernate.
     * 
     * @return o schema configurado como padrão no hibernate.
     * @throws SQLException se houver erro de sql obtendo o schema.
     */
    public String getDBSchema() throws SQLException {
        String schema = this.createHibernatePropertiesInstance().getProperty("hibernate.default_schema");
        if (schema == null) {
            DataSource ds = this.createDataSourceInstance();
            BasicDataSource basicDataSource = (BasicDataSource) ds;
            schema = basicDataSource.getUsername();
        }
        return schema;
    }

    /**
     * Cria, utilizando os dados do spring, as properties que serão utilizadas pelo hibernate.
     * 
     * @return as properties que serão utilizadas pelo hibernate.
     */
    public Properties createHibernatePropertiesInstance() {

        Properties defaultProp = (Properties) this.bf.getBean(SpringConfigFactory.HIBERNATE_PROPERTIES_BEAN_NAME, Properties.class);
        Properties hibernateProp = new Properties();

        for (Enumeration<?> e = defaultProp.propertyNames(); e.hasMoreElements();) {
            String key = (String) e.nextElement();
            hibernateProp.setProperty(key, defaultProp.getProperty(key));
        }
        return hibernateProp;

    }

    /**
     * Inicializa o TestDBServer
     */
    private void initializeTestDBServer() {
        this.server = this.createTestDBServerInstance();
        DataSource dataSource = this.createDataSourceInstance();

        String schema = null;
        try {
            schema = this.getDBSchema();
        } catch (SQLException e) {
            throw new TestDBServerException("Nao foi possivel obter schema do banco de dados", e);
        }

        BasicDataSource basicDataSource = (BasicDataSource) dataSource;

        this.server.setDataSource(basicDataSource);
        this.server.setDefaultSchema(schema);

        if (this.server instanceof ManagedTestDBServer) {
            ManagedTestDBServer managedServer = (ManagedTestDBServer) this.server;
            managedServer.start();
        }
        LOG.debug("TestDBServer configured, ds: " + basicDataSource.getUrl() + ", user: " + basicDataSource.getUsername() + ", schema: "
                + schema);
    }

    /**
     * Devolve a chave do schema de base de dados atual.
     * 
     * @return a chave do schema de base de dados atual.
     */
    public String getSchemaKey() {
        return this.schemaKey;
    }

    /**
     * Devolve a chave dos dados da base de dados atual.
     * 
     * @param type o tipo de base de referência.
     * @return a chave dos dados da base de dados atual.
     */
    public String getDataKey(ReferenceDatabaseDescriptionType type) {
        return this.dataKey.get(type);
    }

    /**
     * Devolve o MD5 do schema de base de dados atual.
     * 
     * @return o MD5 gerado.
     */
    public String getSchemaMD5() {
        return this.schemaMD5;
    }

    /**
     * Devolve o MD5 dos dados da base de dados atual.
     * 
     * @param type o tipo de base de referência.
     * @return o MD5 gerado.
     */
    public String getDataMD5(ReferenceDatabaseDescriptionType type) {
        return this.dataMD5.get(type);
    }

    /**
     * Testa se o MD5 do schema está atualizado.
     * 
     * @return <code>true</code> caso o MD5 esteja atualizado, <code>false</code> caso contrário.
     */
    public boolean isSchemaMD5UpToDate() {
        String loadedMD5 = BuildData.loadMD5(this.getSchemaKey());
        if (loadedMD5 == null) {
            return false;
        }

        return loadedMD5.equals(this.getSchemaMD5());
    }

    /**
     * Testa se o MD5 dos dados está atualizado.
     * 
     * @param type o tipo da base de referência.
     * @return <code>true</code> caso o MD5 esteja atualizado, <code>false</code> caso contrário.
     */
    public boolean isDataMD5UpToDate(ReferenceDatabaseDescriptionType type) {
        String loadedMD5 = BuildData.loadMD5(this.getDataKey(type));
        if (loadedMD5 == null) {
            return false;
        }

        return loadedMD5.equals(this.getDataMD5(type));
    }

    /**
     * Salva o estado atual do banco de dados caso ele seja um <code>ManagedTestDBServer</code>.
     * 
     * @param type o tipo da base de referência.
     */
    public void save(ReferenceDatabaseDescriptionType type) {
        if (this.server instanceof ManagedTestDBServer) {
            ManagedTestDBServer managedServer = (ManagedTestDBServer) this.server;

            managedServer.save(this.getDataKey(type));
            BuildData.saveMD5(this.getSchemaKey(), this.getSchemaMD5());
            BuildData.saveMD5(this.getDataKey(type), this.getDataMD5(type));
        }
    }

    /**
     * Restaura um estado do banco de dados caso ele seja um <code>ManagedTestDBServer</code>.
     * 
     * @param type o tipo da base de referência.
     * @return <code>true</code> caso o arquivo seja restaurado com sucesso, <code>false</code> caso contrário.
     */
    public boolean restore(ReferenceDatabaseDescriptionType type) {
        if (!(this.server instanceof ManagedTestDBServer)) {
            return false;
        }

        if (!this.isSchemaMD5UpToDate() || !this.isDataMD5UpToDate(type)) {
            return false;
        }

        ManagedTestDBServer managedServer = (ManagedTestDBServer) this.server;
        long start = System.currentTimeMillis();
        try {
            return managedServer.restore(this.getDataKey(type));
        } finally {
            LOG.debug(String.format("DBLoadedServerUtils: Database type %s restored in %s millis.", type, System.currentTimeMillis()
                    - start));
        }
    }

    /**
     * Seta o status da authorizacao para a thread corrente.
     * 
     * @param authorizationStatusContext authorizationStatusContext
     */
    public void setThreadAuthorizationStatus(AuthorizationStatus authorizationStatusContext) {
        this.authorizationManager.setThreadAuthorizationStatus(authorizationStatusContext);
    }

    /**
     * Devolve o status da autorizacao da thread corrente.
     * 
     * @return o status da autorizacao da thread corrente
     */
    public AuthorizationStatus getThreadAuthorizationStatus() {
        return this.authorizationManager.getThreadAuthorizationStatus();
    }

    /*
     * Metodos utilizados para carregar os dados iniciais no banco de dados
     */
    /**
     * Metodo responsavel por deletar do banco de dados os dados presentes nas tabelas
     */
    public void clearDBData() {
        LOG.trace("clearDBData()");
        long start = System.currentTimeMillis();
        try {
            this.server.clearDBData("ctrl_seq");
        } catch (SQLException e) {
            throw new TestDBServerException(e);
        } finally {
            LOG.debug(String.format("DBLoadedServerUtils: Database data wiped in %s millis.", System.currentTimeMillis() - start));
        }
    }

    /**
     * Metodo responsavel por criar as tabelas no banco de dados.
     * 
     * @throws CouldNotExecuteException se o script nao puder ser executado.
     */
    public void createDBSchema() throws CouldNotExecuteException {
        LOG.trace("createDBSchema()");
        /* Executar os scripts SQL no banco de dados */
        Connection conn = null;
        try {
            conn = this.server.getConnection();

            DbExecutor dbExecutor = this.dbUtilsFactory.getDbExecutor(null, conn);
            for (String statement : this.schemaStatements) {
                dbExecutor.executeSingleStatement(statement);
            }

            File tmpExtraSchemaFile = File.createTempFile("TMP_", "_unit_tests.sql");
            tmpExtraSchemaFile.deleteOnExit();
            List<ReferenceDatabaseEntry> extraSchema = this.getRefdb(ReferenceDatabaseDescriptionType.SCHEMA);
            for (ReferenceDatabaseEntry entry : extraSchema) {
                URL url = this.getClass().getClassLoader().getResource(entry.getClasspathResourcePath());
                DbConverter dbConverterExtraSchema = this.dbUtilsFactory.getDbConverter(url, tmpExtraSchemaFile);
                dbConverterExtraSchema.convert();
                DbExecutor dbExecutorExtraSchema = this.dbUtilsFactory.getDbExecutor(tmpExtraSchemaFile.toURI().toURL(), conn);
                dbExecutorExtraSchema.execute(false);
            }
        } catch (IOException e) {
            throw new CouldNotExecuteException(UtilsMessages.IOEXCEPTION.create(), e);
        } catch (CouldNotConvertException e) {
            throw new CouldNotExecuteException(e.getLocalizedMessageHolder(), e);
        } catch (SQLException e) {
            throw new CouldNotExecuteException(UtilsMessages.SQLEXCEPTION.create(), e);
        } finally {
            if (conn != null) {
                try {
                    conn.close();
                } catch (SQLException e) {
                    LOG.error(e.getMessage(), e);
                }
            }
        }
    }

    /**
     * Devolve o arquivos contendo os recursos de base de referência.
     * 
     * @return o arquivos contendo os recursos de base de referência.
     */
    public String[] getRefDbResources() {
        return this.refDbResources;
    }

    /**
     * Devolve as entradas de base de referência de um determinado tipo.
     * 
     * @param type o tipo de base de referência que deve ser utilizado.
     * @return lista com as URLs
     */
    public List<ReferenceDatabaseEntry> getRefdb(ReferenceDatabaseDescriptionType type) {
        if (!this.refdbDescriptions.isEmpty()) {
            List<ReferenceDatabaseEntry> list = new LinkedList<ReferenceDatabaseEntry>();
            for (ReferenceDatabaseDescriptionType action : type.getActions()) {
                list.addAll(FlatReferenceDatabaseBuilder.flatten(this.refdbDescriptions, action));
            }
            return list;
        } else {
            return Collections.emptyList();
        }
    }

    /**
     * Devolve as entradas de base de referência de um determinado tipo.
     * 
     * @param action o tipo de base de referência que deve ser utilizado.
     * @return lista com as URLs
     */
    public List<ReferenceDatabaseEntry> getStrictRefdb(ReferenceDatabaseDescriptionType action) {
        if (!this.refdbDescriptions.isEmpty()) {
            return FlatReferenceDatabaseBuilder.flatten(this.refdbDescriptions, action);
        } else {
            return Collections.emptyList();
        }
    }

    /**
     * Devolve uma transação.
     * 
     * @return uma transação.
     */
    public TransactionStatus getTransactionStatus() {
        return this.txManager.getTransaction(this.txDefinition);
    }

    /**
     * Devolve uma transação na base quente.
     * 
     * @param pureSessionOnly ver {@link PartitionTransactionDefinition}
     * @return uma transação.
     */
    public TransactionStatus getTransactionStatus(boolean pureSessionOnly) {
        return this.getTransactionStatus(AuthorizationContext.hot, pureSessionOnly);
    }

    /**
     * Devolve uma transação no contexto especificado.
     * 
     * @param partition contexto.
     * @return transação.
     */
    public TransactionStatus getTransactionStatus(AuthorizationContext partition) {
        return this.getTransactionStatus(partition, false);
    }

    /**
     * Devolve uma transação no contexto especificado.
     * 
     * @param partition contexto.
     * @param pureSessionOnly ver {@link PartitionTransactionDefinition}
     * @return transação.
     */
    public TransactionStatus getTransactionStatus(AuthorizationContext partition, boolean pureSessionOnly) {
        PartitionTransactionDefinition def = new PartitionTransactionDefinition();
        def.setPartition(partition);
        def.setPureSessionOnly(pureSessionOnly);
        def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
        return this.txManager.getTransaction(def);
    }

    /**
     * Executa commit na transação.
     * 
     * @param txStatus transação.
     */
    public void commit(TransactionStatus txStatus) {
        DBEnvironmentHolder.getInstance().getEnvironment().markDirty();
        this.txManager.commit(txStatus);
    }

    /**
     * Executa rollback na transação.
     * 
     * @param txStatus transação.
     */
    public void rollback(TransactionStatus txStatus) {
        this.txManager.rollback(txStatus);
    }

    /**
     * Define o contexto na txDefinition.
     * 
     * @param partition contexto.
     * @deprecated usar {@link #getTransactionStatus(AuthorizationContext)}
     */
    @Deprecated
    public void setPartition(AuthorizationContext partition) {
        this.txDefinition.setPartition(partition);
    }

    /**
     * Devolve a session factory.
     * 
     * @return session factory.
     */
    public PartitionSessionFactory getSessionFactory() {
        return this.sessionFactory;
    }

    /**
     * Devolve o auditManager.
     * 
     * @return o auditManager.
     */
    public AuditManager getAuditManager() {
        return this.auditManager;
    }

    /**
     * Devolve a txDefinition.
     * 
     * @return txDefinition.
     * @deprecated se vc tem um motivo justo para usar esse método solicite que o 'deprecated' seja retirado no através do JIRA
     */
    @Deprecated
    public PartitionTransactionDefinition getTxDefinition() {
        return this.txDefinition;
    }

    /**
     * @param txDefinition transaction definition
     * @deprecated se vc tem um motivo justo para usar esse método solicite que o 'deprecated' seja retirado no através do JIRA
     */
    @Deprecated
    public void setTxDefinition(PartitionTransactionDefinition txDefinition) {
        this.txDefinition = txDefinition;
    }

    /**
     * Finaliza o servidor de banco de dados, caso o servidor suporte essa operação.
     */
    public void shutdown() {
        if (this.server instanceof ManagedTestDBServer) {
            ManagedTestDBServer managedServer = (ManagedTestDBServer) this.server;
            managedServer.stop();
        }
    }

    /***************************************************************************************************************************************
     * Utilitários
     **************************************************************************************************************************************/

    /**
     * @deprecated se vc tem um motivo justo para usar esse método solicite que o 'deprecated' seja retirado no através do JIRA
     * @return um numero sequencial potencialmente repetido.
     */
    @Deprecated
    public long generateSequencial() {
        Date date = new Date();
        return date.getTime();
    }

    /**
     * @param nameBO string
     * @return bo
     * @deprecated remover esse método após 01/01/2009
     */
    @Deprecated
    public Object getBO(String nameBO) {
        Session session = this.getSessionFactory().getCurrentSession();

        List<?> list = session.createQuery("from " + nameBO).list();
        if (list.size() > 0) {
            return list.get(0);
        }

        return null;
    }

    /**
     * Devolve a bean factory.
     * 
     * @return bean factory.
     */
    public BeanFactory getBeanFactory() {
        return this.bf;
    }

    /**
     * devolve a validator factory.
     * 
     * @return validator factory.
     */
    public ValidatorFactory getValidatorFactory() {
        return this.validatorFactory;
    }

    /**
     * Descarta as tabelas do banco de dados.
     * 
     * @throws SQLException se a operação não puder ser executada.
     */
    public void dropTables() throws SQLException {
        LOG.trace("dropTables()");
        this.server.dropTables();
    }

    /**
     * @return the sqlData
     */
    public List<URL> getSqlData() {
        return this.sqlData;
    }

    /**
     * @param sqlData the sqlData to set
     */
    public void setSqlData(List<URL> sqlData) {
        this.sqlData = sqlData;
    }

    /**
     * Inicializa a conexão caso ela não exista.
     * 
     * @throws SQLException exceção.
     */
    public void initConnection() throws SQLException {
        if ((this.connection == null) || this.connection.isClosed()) {
            this.connection = this.server.getConnection();
        }
    }

    /**
     * Fecha a conexão se for possível.
     * 
     * @throws SQLException exceção.
     */
    public void closeConnection() throws SQLException {
        if ((this.connection != null) && !this.connection.isClosed()) {
            this.connection.close();
            this.connection = null;
        }
    }

    /**
     * @param url o url do arquivo a ser executado. Atenção, não há garantia que a conexão será fechada.
     * @throws SQLException exceção.
     * @throws CouldNotExecuteException exceção
     */
    public void executeFile(URL url) throws SQLException, CouldNotExecuteException {
        try {
            this.enableBatchMode(this.connection);
            this.dbUtilsFactory.getDbExecutor(url, this.connection).execute();
            this.disableBatchMode(this.connection);
        } catch (CouldNotExecuteException ex) {
            this.connection.rollback();
            this.connection.close();
            throw ex;
        }
    }

    /**
     * Habilita o modo batch.
     * 
     * @param conn connection
     */
    private void enableBatchMode(Connection conn) {
        if (this.server instanceof ManagedTestDBServer) {
            ManagedTestDBServer managedServer = (ManagedTestDBServer) this.server;
            managedServer.enableBatchMode(conn);
        }
    }

    /**
     * Desabilita o modo batch.
     * 
     * @param conn connection
     */
    private void disableBatchMode(Connection conn) {
        if (this.server instanceof ManagedTestDBServer) {
            ManagedTestDBServer managedServer = (ManagedTestDBServer) this.server;
            managedServer.disableBatchMode(conn);
        }
    }
}
